<?php
/**
 * The MIT License
 *
 * Copyright (c) <year> <copyright holders>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * Classe per il controllo e la gestione delle routes
 *
 * @package lib
 * @subpackage router
 * @author Damiano Ciarla
 */
class Router
{
	/**
	 * Lista delle route da verificare
	 *
	 * @var array|Route
	 */
	protected $_routes = array();

	/**
	 * Istanza della classe Router
	 *
	 * @var Router
	 */
	private static $instance;

	/**
	 * Metodo che restituisce l'istanze di questa classe implementando il pattern Singleton
	 *
	 * @return Router
	 */
	public static function getInstance()
	{
		if (self::$instance === null)
			self::$instance = new Router();

		return self::$instance;
	}

	/**
	 * Metodo che aggiunge una route da inserire nel controllo della url
	 *
	 * @param string $route Route da verificare
	 * @param string $controller Nome del controller
	 * @param string $action Nome della action
	 * @param array $otherParam Parametri aggiuntivi da restituire nel caso che il controllo della route abbia esito positivo
	 *
	 * @return Route
	 */
	public function route($name, $route, $controller, $action, array $otherParam = array())
	{
		$route = new Route($name, $route, $controller, $action, $otherParam);
		$this->_routes[$name] = $route;

		return $route;
	}

	/**
	 * Metodo che restituisce l'url partendo dai parametri della route
	 *
	 * @param string $name Nome della route
	 * @param array $params Lista dei parametri da sostituire ai placemark
	 *
	 * @return string
	 */
	public function url($name, array $params = null)
	{
		if(!count($this->_routes))
			throw new Router_Exception("", Router_Exception::NO_ROUTES_DEFINED);

		if(!isset($this->_routes[$name]))
			throw new Router_Exception($name, Router_Exception::ROUTES_NOT_FOUND);

		$route = $this->_routes[$name];

		$allParameters = $route->namedParams();
		$requiredParameters = array_diff_key($route->namedParams(), $route->optionalParams());

		if(count($requiredParameters) && $params === null)
			throw new Router_Exception("", Router_Exception::PARAMS_CAN_NOT_BE_NULL);

		if(count($requiredParameters) && count($params) < count($requiredParameters))
			throw new Router_Exception("", Router_Exception::WRONG_NUMBER_OF_PARAMETERS);

		if($params === null || !is_array($params))
			$params = array();
		
		$paramsNotFound = array_diff_key($params, $route->namedParams());

		if(count($paramsNotFound))
			throw new Router_Exception(implode(", ", array_keys($paramsNotFound)), Router_Exception::SOME_PARAMETERS_ARE_NOT_PRESENT_INTO_ROUTE_PARAMS);

		$routeUrl = $route->route();

		foreach($requiredParameters as $paramName => $placeholder)
		{
			if(!array_key_exists($paramName, $params))
				throw new Router_Exception($paramName, Router_Exception::REQUIRED_PARAM_NOT_FOUND);

			$routeUrl = str_replace($placeholder, $params[$paramName], $routeUrl);
		}

		$optionalParameters = array_intersect_key($route->optionalParamSegment(), $route->namedParams());

		$foundParameters = array();

		foreach($optionalParameters as $paramName => $placeholder)
		{
			if(array_key_exists($paramName, $params))
			{
				$foundParameters[] = true;
				$routeUrl = str_replace($placeholder, str_replace(array("(",")"), array("",""), $placeholder), $routeUrl);
				$routeUrl = str_replace($allParameters[$paramName], $params[$paramName], $routeUrl);
			}
			else
			{
				$foundParameters[] = false;
				$routeUrl = str_replace($placeholder, "", $routeUrl);
			}
		}

		for($i = 0; $i < count($foundParameters); $i++)
		{
			if($i > 0 && $foundParameters[$i] && !$foundParameters[$i - 1])
				throw new Router_Exception($paramName, Router_Exception::INCORRECT_ORDER_FOR_OPTIONAL_PARAMETERS);
		}

		return "/" . $routeUrl;
	}

	/**
	 * Metodo che avvia il routing della URL in base al tipo di richiesta HTTP.
	 * Sarà restituito un array contenente i parametri trovati nella route o un
	 * valore boolean "false" che indica che nessuna route è stata trovata
	 *
	 * @param string $url Url da controllare
	 * @param string $method Tipologia della richiesta HTTP
	 *
	 * @return boolean|array
	 */
	public function match($url, $method = null)
	{
		$params = false;

		if(!count($this->_routes))
			return $params;

		if($method === null)
			$method = $_SERVER['REQUEST_METHOD'];

		$url = trim($url, '/');

		foreach($this->_routes as $name => $route)
		{
			$params = $this->_routeMatch($route, $method, $url);

			if($params !== false)
			{
				if($route->redirect301() !== null)
				{
					header("HTTP/1.1 301 Moved Permanently");
					header("Location: " . $route->redirect301());
					exit();
				}

				break;
			}
		}
		
		if($params)
		{
			foreach($params as $key => $value)
			{
				switch($key)
				{
					case "controller":
						Bxd::getInstance()->bxdConf()->controller = $value;
						break;

					case "action":
						Bxd::getInstance()->bxdConf()->action = $value;
						break;

					default:
						Bxd::getInstance()->bxdConf()->params[$key] = $value;
						break;
				}
			}
		}

		return $params;
	}

	/**
	 * Metodo che eseguo il controllo di validità di una route in base alla url da controllare
	 * e restituisce un array con eventuali parametri impostati nella descrizione della route
	 *
	 * @param Route $route Istanza della route
	 * @param string $method Tipologia della richiesta HTTP
	 * @param stirng $url Url da controllare
	 *
	 * @return boolean|array
	 */
	private function _routeMatch(Route $route, $method, $url)
	{
		$params = array();

		if($route->isStatic())
		{
			$routeUrl = $route->route();

			if($routeUrl == $url)
				$params = array_merge($route->defaults(), $route->getParamsMethod($method));
		}
		else
		{
			$result = preg_match($route->regexp(), $url, $matches);

			if($result)
			{
				array_shift($matches);

				$namedParams = array_merge($route->namedParams(), $route->optionalParamDefaults());
				$namedParamsNotOptional = array_diff_key($namedParams, $route->optionalParamDefaults());
				$namedParamsMatched = $namedParamsNotOptional;

				$namedParamsIndexed = array_keys($namedParams);

				$mi = count($namedParamsNotOptional);

				while(count($matches) > $mi)
				{
					$namedParamsMatched[$namedParamsIndexed[$mi]] = $namedParams[$namedParamsIndexed[$mi]];
					$mi++;
				}

				if(count($namedParamsMatched) != count($matches))
					throw new Exception("Error matching URL to route params: matched(" . count($matches) . ") != named(" . count($namedParamsMatched) . ")");

				if(count($namedParamsMatched) && count($matches))
					$params = array_combine(array_keys($namedParamsMatched), $matches);

				if(strtoupper($method) != "GET")
				{
					$params = array_merge($namedParams, $route->defaults(), $params, $route->getParamsMethod($method));
				}
				else
				{
					$params = array_merge($namedParams, $route->defaults(), $route->getParamsMethod($method), $params);
				}
			}
		}

		return ((count($params)) ? $params : false);
	}
}
?>